Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / core / data / src / commonTest / kotlin / org / meshtastic / core / data / repository / RadioConfigRepositoryImplTest.kt

Displaying Raw • Download

core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImplTest.kt 01cd54907d62cd90913d3d071b6bc240cae365ef (01cd5490) Text, 5.91 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.data.repository

Tff7b72import T7ee787androidx.datastore.core.DataStore
Tff7b72import T7ee787dev.mokkery.MockMode
Tff7b72import T7ee787dev.mokkery.mock
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.ExperimentalCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.async
Tff7b72import T7ee787kotlinx.coroutines.awaitAll
Tff7b72import T7ee787kotlinx.coroutines.coroutineScope
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787kotlinx.coroutines.withContext
Tff7b72import T7ee787org.meshtastic.core.data.datasource.SwitchingChannelSetDataSource
Tff7b72import T7ee787org.meshtastic.core.database.DatabaseProvider
Tff7b72import T7ee787org.meshtastic.core.datastore.LocalConfigDataSource
Tff7b72import T7ee787org.meshtastic.core.datastore.ModuleConfigDataSource
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.repository.NodeRepository
Tff7b72import T7ee787org.meshtastic.proto.FileInfo
Tff7b72import T7ee787org.meshtastic.proto.LocalConfig
Tff7b72import T7ee787org.meshtastic.proto.LocalModuleConfig
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals

Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72class T56d364RadioConfigRepositoryImplTest Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3nodeDB Tff7b72= Te6edf3mockTff7b72<Te6edf3NodeRepositoryTff7b72>Tb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)

T8b949e// These data sources are concrete wrappers -- construct them with mocked dependencies. Channels are now stored
T8b949e// per-device via SwitchingChannelSetDataSource (DatabaseProvider), the others still wrap a DataStore<T>. None are
T8b949e// touched by the addFileInfo/fileManifestFlow/clearFileManifest behavior under test here.
Tff7b72private Tff7b72val Te6edf3channelSetDataSource Tff7b72=
Te6edf3SwitchingChannelSetDataSourceTb4b4b4(
Te6edf3mockTff7b72<Te6edf3DatabaseProviderTff7b72>Tb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4,
Te6edf3CoroutineDispatchersTb4b4b4(
Te6edf3main Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Te6edf3io Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Te6edf3default Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3localConfigDataSource Tff7b72= Te6edf3LocalConfigDataSourceTb4b4b4(Te6edf3mockTff7b72<Te6edf3DataStoreTff7b72<Te6edf3LocalConfigTff7b72>Tff7b72>Tb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3moduleConfigDataSource Tff7b72= Te6edf3ModuleConfigDataSourceTb4b4b4(Te6edf3mockTff7b72<Te6edf3DataStoreTff7b72<Te6edf3LocalModuleConfigTff7b72>Tff7b72>Tb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffcreateRepositoryTb4b4b4(Tb4b4b4) Tff7b72=
Te6edf3RadioConfigRepositoryImplTb4b4b4(Te6edf3nodeDBTb4b4b4, Te6edf3channelSetDataSourceTb4b4b4, Te6edf3localConfigDataSourceTb4b4b4, Te6edf3moduleConfigDataSourceTb4b4b4)

T8b949e// Regression guard for a real growth vector found via adversarial mesh-fuzz testing: a rogue/hostile
T8b949e// peer can emit FileInfo packets in a tight loop for the entire lifetime of a session -- nothing but
T8b949e// the *next* handshake's clearFileManifest() bounds this accumulator otherwise (addFileInfo used to be
T8b949e// an unconditional `_fileManifestFlow.value += info`). A real device's manifest is bounded by its own
T8b949e// flash storage, so this only bites under an adversarial/malformed stream, but a rogue node hitting it
T8b949e// for the life of a long-running session is a real unbounded-memory-growth DoS.
Tf0883e@Test
Tff7b72fun Td2a8ff`addFileInfo caps the manifest instead of growing without bound`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3repository Tff7b72= Te6edf3createRepositoryTb4b4b4(Tb4b4b4)
T8b949e// Mirrors the private MAX_FILE_MANIFEST_ENTRIES cap in RadioConfigRepositoryImpl -- kept as a
T8b949e// literal here rather than exposing the constant, since the cap value itself isn't the contract
T8b949e// under test (that it caps at all, deterministically, is).
Tff7b72val Te6edf3cap Tff7b72= T79c0ff4T79c0ff0T79c0ff9T79c0ff6

Te6edf3repeatTb4b4b4(Te6edf3cap Tff7b72+ T79c0ff5T79c0ff0T79c0ff0Tb4b4b4) Tb4b4b4{ Te6edf3i Tff7b72-Tff7b72> Te6edf3repositoryTb4b4b4.Te6edf3addFileInfoTb4b4b4(Te6edf3FileInfoTb4b4b4(Te6edf3file_name Tff7b72= Ta5d6ff"Ta5d6fffileTffd700$Te6edf3iTa5d6ff.binTa5d6ff"Tb4b4b4, Te6edf3size_bytes Tff7b72= Te6edf3iTb4b4b4)Tb4b4b4) Tb4b4b4}

Tff7b72val Te6edf3manifest Tff7b72= Te6edf3repositoryTb4b4b4.Te6edf3fileManifestFlowTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3capTb4b4b4, Te6edf3manifestTb4b4b4.Te6edf3sizeTb4b4b4)
Tb4b4b4}

T8b949e// Regression guard for the read-then-write race in addFileInfo (flagged in review): handleFileInfo
T8b949e// dispatches each packet on its own scope.handledLaunch, so addFileInfo runs concurrently. The old
T8b949e// `val c = flow.value; flow.value = c + info` could drop updates (two callers read the same list,
T8b949e// one append is lost) or bypass the cap. update{} makes it atomic. Firing many concurrent adds below
T8b949e// the cap must land every one -- no lost updates. (Trivially true when single-threaded; actually
T8b949e// exercises the race on multi-threaded targets like the JVM.)
Tf0883e@Test
Tff7b72fun Td2a8ff`concurrent addFileInfo does not lose updates`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3repository Tff7b72= Te6edf3createRepositoryTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3n Tff7b72= T79c0ff2T79c0ff0T79c0ff0T79c0ff0 T8b949e// below the 4096 cap, so every add must be retained

Te6edf3withContextTb4b4b4(Te6edf3DispatchersTb4b4b4.Te6edf3DefaultTb4b4b4) Tb4b4b4{
Te6edf3coroutineScope Tb4b4b4{
Tb4b4b4(T79c0ff0 Te6edf3until Te6edf3nTb4b4b4)
Tb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3i Tff7b72-Tff7b72> Te6edf3async Tb4b4b4{ Te6edf3repositoryTb4b4b4.Te6edf3addFileInfoTb4b4b4(Te6edf3FileInfoTb4b4b4(Te6edf3file_name Tff7b72= Ta5d6ff"Ta5d6fffTffd700$Te6edf3iTa5d6ff.binTa5d6ff"Tb4b4b4, Te6edf3size_bytes Tff7b72= Te6edf3iTb4b4b4)Tb4b4b4) Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3awaitAllTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Te6edf3assertEqualsTb4b4b4(Te6edf3nTb4b4b4, Te6edf3repositoryTb4b4b4.Te6edf3fileManifestFlowTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`clearFileManifest empties the manifest so a new handshake can accumulate again`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3repository Tff7b72= Te6edf3createRepositoryTb4b4b4(Tb4b4b4)
Te6edf3repositoryTb4b4b4.Te6edf3addFileInfoTb4b4b4(Te6edf3FileInfoTb4b4b4(Te6edf3file_name Tff7b72= Ta5d6ff"Ta5d6ffa.binTa5d6ff"Tb4b4b4, Te6edf3size_bytes Tff7b72= T79c0ff1Tb4b4b4)Tb4b4b4)
Te6edf3repositoryTb4b4b4.Te6edf3addFileInfoTb4b4b4(Te6edf3FileInfoTb4b4b4(Te6edf3file_name Tff7b72= Ta5d6ff"Ta5d6ffb.binTa5d6ff"Tb4b4b4, Te6edf3size_bytes Tff7b72= T79c0ff2Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3repositoryTb4b4b4.Te6edf3fileManifestFlowTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4)

Te6edf3repositoryTb4b4b4.Te6edf3clearFileManifestTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3repositoryTb4b4b4.Te6edf3fileManifestFlowTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4)

Te6edf3repositoryTb4b4b4.Te6edf3addFileInfoTb4b4b4(Te6edf3FileInfoTb4b4b4(Te6edf3file_name Tff7b72= Ta5d6ff"Ta5d6ffc.binTa5d6ff"Tb4b4b4, Te6edf3size_bytes Tff7b72= T79c0ff3Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3repositoryTb4b4b4.Te6edf3fileManifestFlowTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.05s